import os
import shutil
from collections import defaultdict

import pandas as pd
# ---------------------- 带背景和标题的登录对话框 --------------------
from PySide6.QtCore import QCoreApplication, QEvent, QTimer, Qt,QUrl
from PySide6.QtGui import QIcon, QAction
from PySide6.QtSql import QSqlDatabase, QSqlQuery
from PySide6.QtWebEngineCore import QWebEngineSettings  # 注意模块路径变化
from PySide6.QtWebEngineWidgets import QWebEngineView
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QTreeWidget, QTreeWidgetItem,
                               QTableWidget, QTableWidgetItem, QComboBox,
                               QFileDialog, QHeaderView, QAbstractItemView, QProgressDialog, QMenu, QToolBar,
                               QToolTip, QSplitter, QGridLayout, QLabel, QLineEdit, QPushButton,
                               QVBoxLayout, QHBoxLayout, QMessageBox)


def load_combobox_data(combo, field_name, placeholder):
    """从数据库加载下拉框数据"""
    combo.clear()
    # 添加空项作为默认首选项
    combo.addItem("")
    try:
        # 执行安全查询（包含NULL值过滤）
        query = QSqlQuery()
        sql = f"""
                    SELECT DISTINCT `{field_name}` 
                    FROM patents 
                    WHERE TRIM(`{field_name}`) != ''  -- 排除空字符串
                    ORDER BY `{field_name}`
                """
        if query.exec(sql):
            # has_valid_data = False
            while query.next():
                value = query.value(0)
                if value.strip():  # 过滤空值
                    combo.addItem(value)
                    # 设置当前项的 ToolTip
                    index = combo.count() - 1
                    item = combo.model().item(index)
                    item.setToolTip(value)
        else:
            raise Exception(f"数据库查询失败：{query.lastError().text()}")
    except Exception as e:
        raise Exception(f"加载{field_name}数据失败：{str(e)}")
        combo.setToolTip("数据加载失败，仍可手动输入")
        combo.lineEdit().setPlaceholderText("数据加载失败，请手动输入")
    finally:
        if 'query' in locals(): query.clear()


class PatentApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.tree = None
        self.db = None
        # 定义字段映射列表
        self.field_mappings = [
            # 格式：(数据库字段/控件属性名, placeholder, 控件类型,宽度,数据类型)
            ('id', '序号', 'lineedit', 120,'number'),
            ('parent_branch', '上级分支', 'None', 120,'text'),
            ('field_branch', '所属分支', 'None', 120,'text'),
            ('title_cn', '标题 (中文)', 'lineedit', 120,'text'),
            ('title', '标题 (英文)', 'lineedit', 120,'text'),
            ('abstract_cn', '摘要 (中文)', 'lineedit', 120,'text'),
            ('abstract', '摘要 (英文)', 'lineedit', 120,'text'),
            ('applicant', '申请人', 'lineedit', 120,'text'),
            ('public_number', '公开（公告）号', 'lineedit', 120,'text'),
            ('public_date', '公开（公告）日', 'lineedit', 120,'date'),
            ('application_number', '申请号', 'lineedit', 120,'text'),
            ('application_date', '申请日', 'lineedit', 120,'date'),
            ('public_type', '公开类型', 'combobox', 120,'text'),
            ('patent_type', '专利类型', 'combobox', 120,'text'),
            ('public_country', '公开国别', 'lineedit', 120,'text'),
            ('title_ original', '标题（小语种原文）', 'lineedit', 120,'text'),
            ('abstract_ original', '摘要（小语种原文）', 'lineedit', 120,'text'),
            ('first_claim', '首项权利要求', 'lineedit', 120,'text'),
            ('first_claim_translation', '首权翻译', 'lineedit', 120,'text'),
            ('pages', '文献页数', 'None', 120,'text'),
            ('first_claim_words', '首权字数', 'lineedit', 120,'number'),
            ('main_ipc', 'IPC主分类', 'combobox', 120,'text'),
            ('ipc', 'IPC', 'combobox', 120,'text'),
            ('cpc', 'CPC', 'combobox', 120,'text'),
            ('standard_applicant', '标准化申请人', 'lineedit', 120,'text'),
            ('standard_current_patent_holder', '标准化当前权利人', 'lineedit', 120,'text'),
            ('current_patent_holder', '当前权利人', 'lineedit', 120,'text'),
            ('applicant_country', '申请人国家/地区', 'lineedit', 120,'text'),
            ('invetor', '发明人', 'lineedit', 120,'text'),
            ('effective_country', 'EP指定生效国', 'combobox', 120,'text'),
            ('citing_patent', '引证专利', 'lineedit', 120,'text'),
            ('cited_patent', '被引证专利', 'lineedit', 120,'text'),
            ('cited_family', '家族被引证', 'lineedit', 120,'text'),
            ('citing_applicant', '引证申请人', 'lineedit', 120,'text'),
            ('cited_applicant', '被引证申请人', 'lineedit', 120,'text'),
            ('citing_family_applicant', '家族引证申请人', 'lineedit', 120,'text'),
            ('cite_family_applicant', '家族被引证申请人', 'lineedit', 120,'text'),
            ('family_cited_times', '家族被引证次数', 'lineedit', 120,'text'),
            ('cited_country', '被引证国别(forward)', 'combobox', 120,'text'),
            ('simple_family', '简单同族', 'lineedit', 120,'text'),
            ('extend_family', '扩展同族', 'lineedit', 120,'text'),
            ('simple_extend_family', '简单同族个数', 'lineedit', 120,'number'),
            ('extend_family_number', '扩展同族个数', 'lineedit', 120,'number'),
            ('family_country', '同族国家/地区', 'combobox', 120,'text'),
            ('priority_pumber', '优先权号', 'lineedit', 120,'text'),
            ('priority_date', '优先权日', 'lineedit', 120,'date'),
            ('earliest_priority_date', '最早优先权日', 'lineedit', 120,'date'),
            ('priority_country', '优先权国别', 'combobox', 120,'text'),
            ('double_application', '一案双申', 'combobox', 120,'text'),
            ('patent_effectiveness', '专利有效性', 'lineedit', 120,'text'),
            ('current_legal_status', '当前法律状态', 'combobox', 120,'text'),
            ('first_public_number', '首次公开号', 'lineedit', 120,'text'),
            ('first_public_date', '首次公开日', 'lineedit', 120,'date'),
            ('issue_number', '授权公告号', 'lineedit', 120,'text'),
            ('issue_date', '授权公告日', 'lineedit', 120,'date'),
            ('expired_date', '失效日', 'lineedit', 120,'date'),
            ('patent_lifespan _months', '专利寿命（月）', 'lineedit', 120,'number'),
            ('estimated_maturity_date', '预估到期日', 'lineedit', 120,'date'),
        ]
        # 列名映射
        self.column_mapping = [x[0] for x in self.field_mappings if isinstance(x, (tuple, list)) and len(x) >= 1]
        self.search_widgets = []  # 可选：如果需要后续访问控件集合
        self.init_ui()
        self.init_db()
        self.current_sort_column = 0  # 新增：当前排序列索引
        self.current_sort_order = Qt.SortOrder.AscendingOrder  # 新增：当前排序方向
        self.current_page = 1
        self.page_size = 25
        self.total_pages = 1
        self.current_filter = {}
        self.load_tree()
        self.load_data()
        self.showMaximized()
        self.filedir = "pdf"
        # 确保pdf目录存在
        if not os.path.exists(self.filedir):
            os.makedirs(self.filedir)

    def init_ui(self):
        self.setWindowTitle("专利管理系统 - Patent Navigator")
        self.setWindowIcon(QIcon('setting/icon.png'))
        # 设置全局样式
        self.setStyleSheet("""        
            QMainWindow {
                background-color: #f0f4f8; /*浅灰*/
            }
            QWidget {
                font-family: '微软雅黑';
                font-size: 15px;
            }
            QSplitter::handle {
                background: #d1dbe5; /*灰 #f8f9fa */
            }
            QTreeWidget {
                background: #ffffff;
                border: 1px solid #d1dbe5;
                border-radius: 4px;
                padding: 5px;
                alternate-background-color: #d1dbe5;
                font-family: '微软雅黑';
                font-size: 15px;
            }
            QTreeWidget::branch {
                background: transparent;
            }
            QTreeWidget::item:hover {
                background-color: #e6f3ff;
                color: #2c6db3;
            }
            QTreeWidget::item:selected {
                color: white;
                border-radius: 3px;
                border: 1px solid #FF4444;                
            }
            QHeaderView::section {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #5b8cff, stop:1 #4066d6);
                color: white;
                padding: 6px;
                border: 1px solid #4a6cd4;
                font-weight: bold;
            }
             /* 表头背景色 */
            /* QHeaderView::section {
                    border: 1px solid #4066d6;
                    padding: 4px;
                    background-color: #4f7adf; 
                }
                QHeaderView { 
                    border-bottom: 1px solid #D4D4D4;
                }
                */
            QTableWidget {
                background: white;
                alternate-background-color: #f8fafc;
                gridline-color: #e5e9ed;
                border-radius: 5px;
            }
            QTableWidget::item {
                padding: 4px;
            }
            QTableWidget::item:selected {
                background-color: #2c6db3;
                color: white;
            }
            QPushButton {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #5b8cff, stop:1 #4066d6);
                border: 1px solid #4a6cd4;
                color: white;
                border-radius: 4px;
                padding: 3.5px 12px;
                min-width: 20px;
            }
            QPushButton:hover {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #6c9cff, stop:1 #4f7adf);
            }
            QPushButton:pressed {
                background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                    stop:0 #3a5db3, stop:1 #2e4ca3);
            }
            QLineEdit, QComboBox {
                border: 1px solid #d1dbe5;
                border-radius: 3px;
                padding: 5px;
                background: white;
                font-size: 12px; 
            }
            QLineEdit:focus, QComboBox:focus {
                border-color: #5b8cff;
                background: #f0f6ff;
            }
            QTreeWidget {
                show-decoration-selected: 1;
            }
            QTreeWidget::item {
                height: 28px;
                border: 1px solid transparent;
                margin: 2px 0;
            }
            /* 悬停状态 */
            QTreeWidget::branch:hover {
                background-color: #e6f3ff;
            }
            /* 图标对齐调整 */
           /*  QTreeWidget::indicator {
                width: 16px;
                height: 16px;
            } */
            /* 子项缩进 */
            QTreeWidget::item:has-children {
                padding-left: 2px;
            }
            /* 选中状态 */
            QTreeWidget::item:selected {
                background-color: #2c6db3;
                color: white;
                border-radius: 3px;
            }
            QToolTip {
                background-color: #FFFFE0;
                color: #333;
                border: 1px solid #FF4444;
                padding: 4px;
                font-size: 15px; 
            }
            QToolBar {
                    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                        stop:0 #f8fafc, stop:1 #e9eef4);
                    border-bottom: 1px solid #d1dbe5;
                    spacing: 8px;
                    padding: 4px;
                }
                QToolButton {
                    padding: 6px 12px;
                    border-radius: 4px;
                }
                QToolButton:hover {
                    background: #e6f3ff;
                }
        """)
        # 使用 QSplitter 实现可调节宽度的左右布局
        splitter = QSplitter()
        splitter.setHandleWidth(2)

        # 左侧部件
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_widget.setMinimumWidth(150)

        # 技术分支树形控件
        self.tree = QTreeWidget()
        self.tree.setHeaderLabel("📁 技术分支导航")
        self.tree.itemClicked.connect(self.on_tree_click)
        left_layout.addWidget(self.tree)

        # 右侧部件
        right_widget = QWidget()
        right_layout = QVBoxLayout(right_widget)
        right_layout.setContentsMargins(10, 10, 10, 10)
        right_layout.setSpacing(12)
        # 初始化工具栏（自定义样式）
        self.init_toolbar()
        # 搜索栏样式优化
        search_layout = self.init_search_layout()
        # 表格初始化
        self.init_table()
        # 分页组件样式
        page_layout = self.init_page()
        # 组装右侧布局
        right_layout.addLayout(search_layout)
        right_layout.addWidget(self.table, 1)
        right_layout.addLayout(page_layout)

        # Splitter布局
        splitter.addWidget(left_widget)
        splitter.addWidget(right_widget)
        splitter.setSizes([150, 800])
        self.setCentralWidget(splitter)

    def init_db(self):
        # 确保db目录存在
        db_dir = 'setting'
        os.makedirs(db_dir, exist_ok=True)  # 自动创建不存在的目录
        # 构造跨平台兼容的绝对路径
        db_path = os.path.abspath(os.path.join(db_dir, 'patents.db'))
        # 配置数据库连接
        self.db = QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName(db_path)
        if not self.db.open():
            QMessageBox.information(self, "提示", f"无法连接数据库,{self.db.lastError().text()}")
            return False
        query = QSqlQuery()
        query.exec(""" CREATE TABLE IF NOT EXISTS patents (
                                id INT,
                                parent_branch TEXT,
                                field_branch TEXT,
                                title_cn TEXT,
                                title TEXT,
                                abstract_cn TEXT,
                                abstract TEXT,
                                applicant TEXT,
                                public_number TEXT,
                                public_date TEXT,
                                application_number TEXT,
                                application_date TEXT,
                                public_type TEXT,
                                patent_type TEXT,
                                public_country TEXT,
                                title_ original TEXT,
                                abstract_ original TEXT,
                                first_claim TEXT,
                                first_claim_translation TEXT,
                                pages TEXT,
                                first_claim_words TEXT,
                                main_ipc TEXT,
                                ipc TEXT,
                                cpc TEXT,
                                standard_applicant TEXT,
                                standard_current_patent_holder TEXT,
                                current_patent_holder TEXT,
                                applicant_country TEXT,
                                invetor TEXT,
                                effective_country TEXT,
                                citing_patent TEXT,
                                cited_patent TEXT,
                                cited_family TEXT,
                                citing_applicant TEXT,
                                cited_applicant TEXT,
                                citing_family_applicant TEXT,
                                cite_family_applicant TEXT,
                                family_cited_times TEXT,
                                cited_country TEXT,
                                simple_family TEXT,
                                extend_family TEXT,
                                simple_extend_family TEXT,
                                extend_family_number TEXT,
                                family_country TEXT,
                                priority_pumber TEXT,
                                priority_date TEXT,
                                earliest_priority_date TEXT,
                                priority_country TEXT,
                                double_application TEXT,
                                patent_effectiveness TEXT,
                                current_legal_status TEXT,
                                first_public_number TEXT,
                                first_public_date TEXT,
                                issue_number TEXT,
                                issue_date TEXT,
                                expired_date TEXT,
                                patent_lifespan _months TEXT,
                                estimated_maturity_date TEXT
                                                            )
                        """)
        if 'query' in locals(): query.clear()

    def init_page(self):
        # 分页控件
        page_layout = QHBoxLayout()
        self.btn_prev = QPushButton("上一页")
        self.btn_next = QPushButton("下一页")
        self.lbl_page = QLabel()
        self.lbl_total = QLabel()  # 新增：总条数标签
        self.cmb_page_size = QComboBox()
        self.cmb_page_size.addItems(["25", "50", "100"])
        # self.btn_export = QPushButton("导出Excel")
        self.btn_prev.clicked.connect(self.prev_page)
        self.btn_next.clicked.connect(self.next_page)
        self.cmb_page_size.currentTextChanged.connect(self.on_page_size_changed)
        # self.btn_export.clicked.connect(self.export_excel)
        page_layout.addWidget(self.btn_prev)
        page_layout.addWidget(self.btn_next)
        page_layout.addWidget(self.lbl_page)
        page_layout.addWidget(QLabel("每页显示："))
        page_layout.addWidget(self.cmb_page_size)
        page_layout.addWidget(self.lbl_total)  # 添加总条数标签
        page_layout.addStretch()
        # page_layout.addWidget(self.btn_export)
        return page_layout

    def init_table(self):
        # 右侧表格
        self.table = QTableWidget()
        self.table.setColumnCount(57)  # 56+1按钮
        headers = ["操作"]
        self.displayed_field_indices = []  # 新增：记录表格列对应的field_mappings索引
        # self.table.setFont(QFont("微软雅黑", 20))  # 字体名称和大小可自定义
        for idx, x in enumerate(self.field_mappings):
            if idx in [1, 2]:  # 跳过原始第2、3列
                continue
            if isinstance(x, (tuple, list)) and len(x) >= 1:
                headers.append(x[1])
                self.displayed_field_indices.append(idx)  # 记录对应的索引
        self.table.setHorizontalHeaderLabels(headers)
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)  # 默认允许手动调整
        self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.table.horizontalHeader().sectionClicked.connect(self.on_header_clicked)  # 新增：表头点击事件
        self.table.verticalHeader().setVisible(False)  # 隐藏垂直表头
        self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)  # 禁止编辑
        # 添加信号连接
        self.table.cellDoubleClicked.connect(self.on_row_double_clicked)
        # 设置水平表头样式
        header = self.table.horizontalHeader()
        # header.setStyleSheet("""
        #     """)
        header.setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)  # 表头文本居中
        # 允许手动调整列宽
        header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
        # 设置初始最小宽度 边界保护：
        header.setMinimumSectionSize(60)
        self.table.setAlternatingRowColors(True)  # 启用交替行颜色
        # 新增悬停提示功能
        self.table.setMouseTracking(True)  # 启用鼠标移动事件
        self.table.viewport().installEventFilter(self)  # 安装事件过滤器
    # 在窗口类中定义槽函数
    def on_row_double_clicked(self, row, column):
        """双击行触发PDF查看（支持备选路径）"""
        # 从表格获取数据
        main_item = self.table.item(row, 7)  # 主专利号
        alt_item = self.table.item(row, 38)  # 备选专利号
        # if not main_item:
        #     QMessageBox.warning(self, "错误", "缺失主专利号")
        #     return
        self.try_view_pdf(
            main_patent=main_item.text(),
            alt_patents_str=alt_item.text() if alt_item else ""
        )

    # 在类中添加事件过滤器
    def eventFilter(self, source, event):
        """处理表格单元格悬停提示"""
        if event.type() == QEvent.Type.ToolTip and source is self.table.viewport():
            # 获取鼠标位置对应的单元格
            pos = event.pos()
            index = self.table.indexAt(pos)
            # 有效单元格处理
            if index.isValid():
                # 排除按钮列（假设最后一列是按钮）
                if index.column() == self.table.columnCount() - 1:
                    return False
                # 获取单元格内容
                item = self.table.item(index.row(), index.column())
                if item and item.text().strip():
                    # 设置提示文本（带自动换行）
                    QToolTip.showText(
                        event.globalPos(),
                        item.text(),  # f"<div style='width: 300px;word-wrap: break-word;font-size: 15px;border: 1px solid #666;     padding: 4px;  background-color: #FFFFE0;'>{item.text()}</div>",
                        self.table
                    )
                    return True

        # 其他情况隐藏提示
        QToolTip.hideText()
        return super().eventFilter(source, event)

    # 搜索框动态换行
    def init_search_layout(self):
        search_layout = QGridLayout()
        valid_fields = [fm[0] for fm in self.field_mappings]
        # 设置布局的水平和垂直间距
        search_layout.setHorizontalSpacing(5)
        search_layout.setVerticalSpacing(5)
        max_row_width = 1000  # 最大行宽，可根据实际需求调整
        current_row = 0
        current_col = 0
        current_row_used = 0  # 当前行已使用的宽度（包括间距）
        for db_field, placeholder, widget_type, width,data_type in self.field_mappings:
            # 创建控件
            if widget_type == "combobox":
                widget = self.create_combobox(db_field, placeholder, valid_fields)
                # 连接信号：当下拉框内容变化时更新样式
                widget.currentIndexChanged.connect(lambda _, w=widget: self.update_widget_style(w))
                widget.editTextChanged.connect(lambda text, w=widget: self.update_widget_style(w))
            elif widget_type == "lineedit":
                widget = QLineEdit()
                widget.setPlaceholderText(placeholder)
                widget.textChanged.connect(self.on_search_changed)
                # 连接信号：当输入框内容变化时更新样式
                widget.textChanged.connect(lambda text, w=widget: self.update_widget_style(w))
            else:
                continue
            # 设置控件宽度
            widget_width = width
            if "_date" in db_field:
                widget.setFixedWidth(120)
                widget.setPlaceholderText(f"{placeholder} YYYY-MM-DD")
                widget_width = 120
            else:
                widget.setFixedWidth(width)
            widget.setMinimumWidth(60)
            # widget.setStyleSheet("QWidget { border: 1px solid #CCCCCC; }")  # 默认边框颜色
            setattr(self, db_field, widget)
            self.search_widgets.append(widget)
            # 计算所需空间
            spacing = search_layout.horizontalSpacing()
            needed_space = widget_width + (spacing if current_col != 0 else 0)
            # 判断是否需要换行
            if current_row_used + needed_space > max_row_width:
                current_row += 1
                current_col = 0
                current_row_used = widget_width
            else:
                current_row_used += needed_space
            # 添加到布局
            search_layout.addWidget(widget, current_row, current_col)
            current_col += 1
        # 添加“清空查询”按钮
        self.btn_clear = QPushButton("清空查询")
        self.btn_clear.clicked.connect(self.clear_search)
        btn_width = 100
        self.btn_clear.setFixedWidth(btn_width)

        # 计算按钮所需空间
        spacing = search_layout.horizontalSpacing()
        needed_space = btn_width + (spacing if current_col != 0 else 0)

        if current_row_used + needed_space > max_row_width:
            current_row += 1
            current_col = 0
            current_row_used = btn_width
        else:
            current_row_used += needed_space

        search_layout.addWidget(self.btn_clear, current_row, current_col)

        return search_layout

    # ----------------------------
    # 新增方法：动态更新控件样式
    # ----------------------------
    def update_widget_style(self, widget):
        """根据控件内容是否为空更新样式"""
        if isinstance(widget, QLineEdit):
            text = widget.text().strip()
        elif isinstance(widget, QComboBox):
            text = widget.currentText().strip()
        else:
            return
        # 如果有内容则设置高亮样式，否则恢复默认
        if text:
            # 设置红色边框和浅黄色背景
            widget.setStyleSheet("""
                QWidget {
                    border: 1px solid #FF4444;
                    background-color: #FFFFE0;
                }
            """)
        else:
            # 恢复默认样式
            widget.setStyleSheet("QWidget { border: 1px solid #CCCCCC; }")

    def create_combobox(self, field_name, placeholder, valid_fields):
        """创建可编辑的下拉框组件"""
        combo = QComboBox()
        combo.setEditable(True)

        # 设置占位符文本
        line_edit = combo.lineEdit()
        line_edit.setPlaceholderText(placeholder)

        # 安全校验字段名
        if field_name not in valid_fields:
            combo.addItem("字段配置错误")
            combo.setEnabled(False)
            return combo

        # 异步加载数据（避免界面卡顿）
        QTimer.singleShot(0, lambda: load_combobox_data(combo, field_name, placeholder))

        # 信号连接
        combo.currentTextChanged.connect(self.on_search_changed)
        return combo

    # 在窗口resize事件中保持布局合理性
    def resizeEvent(self, event):
        super().resizeEvent(event)
        if self.table.columnCount() > 58:
            self.table.setColumnWidth(58, self.table.width() - 100)  # 动态调整这一列

    def init_toolbar(self):
        # 创建工具栏（沿用之前的删除/导出按钮）
        self.toolbar = QToolBar("主工具栏")
        # self.toolbar.setStyleSheet("""
        #
        #     """)
        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.toolbar)
        # 导入按钮
        self.btn_import = QPushButton("导入Excel")
        self.btn_import.clicked.connect(self.import_excel)
        self.toolbar.addWidget(self.btn_import)
        # 导出按钮
        self.btn_export = QPushButton("导出Excel")
        self.btn_export.clicked.connect(self.export_excel)
        self.toolbar.addWidget(self.btn_export)

        # 删除按钮
        self.btn_delete = QPushButton("清空数据")
        self.btn_delete.setStyleSheet("background-color: #ff4444; color: white;")
        self.btn_delete.clicked.connect(self.delete_all_data)
        self.toolbar.addWidget(self.btn_delete)

    # 右键菜单重置列宽：
    def contextMenuEvent(self, event):
        menu = QMenu(self)
        reset_action = menu.addAction("固定列宽")
        action = menu.exec(self.mapToGlobal(event.pos()))
        if action == reset_action:
            self.table.resizeColumnsToContents()
            # 保持特殊列的设置
            for i in range(self.table.columnCount()):
                self.table.setColumnWidth(i, 120)

    def on_header_clicked(self, column):
        """处理表格标题点击排序"""
        if column == 0:  # 第0列按钮不参与排序
            return
        if (column - 1) >= len(self.displayed_field_indices):
            return  # 避免越界错误
        field_mappings_index = self.displayed_field_indices[column - 1]
        if field_mappings_index == self.current_sort_column:
            # 同一列点击切换排序方向
            self.current_sort_order = Qt.SortOrder.DescendingOrder if self.current_sort_order == Qt.SortOrder.AscendingOrder else Qt.SortOrder.AscendingOrder
        else:
            # 新列点击默认升序
            self.current_sort_column = field_mappings_index
            self.current_sort_order = Qt.SortOrder.AscendingOrder
        self.load_data()

    def load_tree(self):
        self.tree.clear()
        query = QSqlQuery("SELECT DISTINCT parent_branch, field_branch FROM patents")

        hierarchy = defaultdict(list)
        all_nodes = set()

        while query.next():
            parent = query.value(0) if not query.isNull(0) else None
            child = query.value(1)
            hierarchy[parent].append(child)
            all_nodes.add(parent)
            all_nodes.add(child)
        if 'query' in locals(): query.clear()

        # 处理父节点为 None 的子节点作为顶级节点
        none_children = hierarchy.get(None, [])
        none_children_sorted = sorted(none_children)

        # 检测其他根节点（没有父节点的非 None 节点）
        child_nodes = {child for children in hierarchy.values() for child in children}
        other_roots = [
            node for node in hierarchy
            if node not in child_nodes and node is not None
        ]
        other_roots_sorted = sorted(other_roots)

        # 处理孤立节点（不在任何父子关系中）
        orphans = all_nodes - (set(hierarchy.keys()) | child_nodes)
        orphans.discard(None)
        orphans_sorted = sorted(orphans)

        # 合并所有顶级节点，按顺序处理
        top_level_nodes = []
        top_level_nodes.extend(none_children_sorted)
        top_level_nodes.extend(other_roots_sorted)
        top_level_nodes.extend(orphans_sorted)

        # 添加所有顶级节点，并生成层级序号
        for index, node in enumerate(top_level_nodes, 1):
            item = QTreeWidgetItem([f"{index} {node}"])
            # 如果节点有子节点，则设置字体加粗
            if node in hierarchy:
                font = item.font(0)
                font.setBold(True)
                item.setFont(0, font)
            self.tree.addTopLevelItem(item)
            if node in hierarchy:
                self.build_subtree(node, item, hierarchy, parent_index_str=str(index))

    def build_subtree(self, parent_branch, parent_item, hierarchy, parent_index_str):
        children = sorted(hierarchy.get(parent_branch, []))
        for idx, child in enumerate(children, 1):
            child_index_str = f"{parent_index_str}.{idx}"
            child_item = QTreeWidgetItem([f"{child_index_str} {child}"])
            # 如果子节点有子节点，则设置字体加粗
            if child in hierarchy:
                font = child_item.font(0)
                font.setBold(True)
                child_item.setFont(0, font)
            parent_item.addChild(child_item)
            if child in hierarchy:
                self.build_subtree(child, child_item, hierarchy, child_index_str)

    def clear_search(self):
        """清空所有搜索输入框内容并刷新数据"""
        for widget in self.search_widgets:
            # 安全清空操作，确保是QLineEdit控件
            if isinstance(widget, QLineEdit):
                widget.clear()
            elif isinstance(widget, QComboBox):
                # 重置下拉框选中状态
                widget.setCurrentIndex(-1)
                # 如果下拉框可编辑，同时清空编辑框文本
                if widget.isEditable():
                    widget.setEditText('')
        # self.tree.clearSelection()
        # 强制重置树形控件选中状态（新增关键代码）
        self.tree.selectionModel().clear()  # 清除所有选中状态
        self.tree.setCurrentItem(None)  # 重置当前选中项
        self.current_page = 1
        self.page_size = 100
        self.load_data()

    # 分页查询逻辑
    def load_data(self, page=1):
        # """加载数据并支持排序分页"""
        where_clause, params = self.get_query_conditions()
        # 获取排序参数
        sort_column_index = self.current_sort_column
        sort_column = self.column_mapping[sort_column_index]
        # 获取数据类型
        field_info = self.field_mappings[sort_column_index]
        data_type = field_info[4] if len(field_info) >= 5 else 'text'  # 默认文本类型
        # 构建排序表达式
        if data_type == 'number':
            sort_expression = f'CAST({sort_column} AS INTEGER)'
        elif data_type == 'date':
            sort_expression = sort_column  # 假设日期格式可正确排序
        else:
            sort_expression = sort_column
        sort_order = 'ASC' if self.current_sort_order == Qt.SortOrder.AscendingOrder else 'DESC'
        order_clause = f'ORDER BY {sort_expression} {sort_order}'

        # 计算总数和总页数
        count_query = QSqlQuery()
        count_query_str = f"SELECT COUNT(*) FROM patents {where_clause}"

        # 添加错误检查
        if not count_query.prepare(count_query_str):
            QMessageBox.information(self, "提示", f"准备总数查询失败:{count_query.lastError().text()}")
            return
        for i, p in enumerate(params):
            count_query.bindValue(i, p)
        if not count_query.exec():
            QMessageBox.information(self, "提示", f"执行总数查询失败:{count_query.lastError().text()}")
            return
        if count_query.next():
            total_records = count_query.value(0)
        else:
            total_records = 0
        self.total_pages = max((total_records + self.page_size - 1) // self.page_size, 1)

        # 构建分页查询（使用预处理避免SQL注入）
        offset = (self.current_page - 1) * self.page_size
        query_str = f"""
            SELECT * FROM patents
            {where_clause}
            {order_clause}
            LIMIT ? OFFSET ?
        """
        # 将分页参数加入绑定列表
        params.extend([self.page_size, offset])
        query = QSqlQuery()
        if not query.prepare(query_str):
            QMessageBox.information(self, "提示", f"准备查询失败:{query.lastError().text()}")
            return
        for i, p in enumerate(params):
            query.bindValue(i, p)
        if not query.exec():
            QMessageBox.information(self, "提示", f"查询失败:{query.lastError().text()}")
            return
        # 清空并填充表格
        self.table.setRowCount(0)
        while query.next():
            row = self.table.rowCount()
            self.table.insertRow(row)
            target_col = 1  # 表格数据列起始位置（跳过按钮列0）
            self.table.setColumnWidth(0,  100)  # 按钮列宽度单独处理
            self.table.setRowHeight(row, 40)  # 设置行高
            for db_col in range(58): # 0,1,2
                if db_col in [1, 2]:  # 跳过数据库第2、3列（索引1和2）
                    continue
                value = query.value(db_col)
                item = QTableWidgetItem(str(value) if value else "")
                # 设置文本居中：水平居中 + 垂直居中
                item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
                self.table.setItem(row, target_col, item)
                self.table.setColumnWidth(target_col, 60 if target_col==1 else 120) #序号列宽度单独处理
                target_col += 1  # 仅当列未被跳过时递增

            # 添加查看按钮
            btn = QPushButton("阅读PDF")
            # 传递当前行的主专利号和备选专利号
            # 使用闭包安全的方式传递参数
            def make_click_handler(m, a):
                return lambda: self.handle_button_view(m, a)
            btn.clicked.connect(
                make_click_handler(
                    query.value(8),  # 主专利号（数据库第8列）
                    query.value(40)  # 备选专利号（数据库第40列）
                )
            )
            self.table.setCellWidget(row, 0, btn)  # 查看PDF按钮列

        if 'query' in locals(): query.clear()
        # 更新分页显示
        self.lbl_page.setText(f"第 {self.current_page} 页 / 共 {self.total_pages} 页")
        self.lbl_total.setText(f"共 {total_records} 条")
        self.btn_prev.setEnabled(self.current_page > 1)
        self.btn_next.setEnabled(self.current_page < self.total_pages)
        self.lbl_page.setStyleSheet("color: #409EFF;")
        self.lbl_total.setStyleSheet("color: #909399;")

    def try_view_pdf(self, main_patent, alt_patents_str, error_title="错误"):
        """统一尝试查看PDF的方法"""
        # 获取有效路径
        pdf_path = self.resolve_pdf_path(main_patent, alt_patents_str)
        if pdf_path:
            self.view_pdf(pdf_path)
        else:
            # 构造友好错误提示
            tried_paths = [
                os.path.join("pdf", f"{main_patent.strip()}.pdf"),
                *[
                    os.path.join("pdf", f"{p.strip()}.pdf")
                    for p in (alt_patents_str or "").split(';')
                    if p.strip()
                ]
            ]

            error_msg = "未找到相关PDF文件:当前专利和同族专利文件均不存在! \n尝试路径：\n" + "\n".join(
                f"• {path}" for path in tried_paths
            )
            QMessageBox.warning(
                self,
                error_title,
                error_msg,
                QMessageBox.StandardButton.Ok
            )

    def resolve_pdf_path(self, main_patent, alt_patents_str):
        """核心方法：解析有效的PDF路径"""
        # 构造基础路径列表
        pdf_paths = [os.path.join("pdf", f"{main_patent.strip()}.pdf")]
        # 添加备选路径
        if alt_patents_str:
            pdf_paths += [
                os.path.join("pdf", f"{p.strip()}.pdf")
                for p in alt_patents_str.split(';')
                if p.strip()
            ]
        # 返回第一个存在的有效路径
        for path in pdf_paths:
            if os.path.exists(path):
                return path
        return None

    def handle_button_view(self, main_patent, alt_patents):
        """处理按钮点击事件"""
        # 构造所有可能路径
        pdf_paths = [os.path.join("pdf", f"{main_patent}.pdf")]
        if alt_patents:
            pdf_paths += [
                os.path.join("pdf", f"{p.strip()}.pdf")
                for p in alt_patents.split(';') if p.strip()
            ]

        # 检查路径存在性
        valid_path = None
        for path in pdf_paths:
            if os.path.exists(path):
                valid_path = path
                break

        if valid_path:
            self.view_pdf(valid_path)
        else:
            QMessageBox.warning(
                self,
                "错误",
                "未找到相关PDF文件:当前专利和同族专利文件均不存在! \n尝试路径：\n" + "\n".join(
                    f"• {path}" for path in pdf_paths
                )
            )

    def get_query_conditions(self):
        """提取查询条件构建逻辑（支持下拉框特殊处理）"""
        conditions = []
        params = []
        # 处理常规搜索条件
        for db_field, _, item_type, width,_ in self.field_mappings:
            widget = getattr(self, db_field, None)
            if not widget:
                continue
            value = widget.currentText().strip() if item_type == "combobox" else widget.text().strip()
            if value:
                if value == "":
                    conditions.append(f"({db_field} IS NULL OR {db_field} = '')")
                else:
                    if db_field == 'id':
                        # 精确匹配数字ID
                        conditions.append("id = ?")
                        params.append(int(value))  # 确保转换为数字
                    else:
                        # 模糊匹配文本字段
                        conditions.append(f"{db_field} LIKE ?")
                        params.append(f"%{value}%")  # 添加通配符
        # 处理树形目录筛选
        if current_item := self.tree.currentItem():
            has_custom_branch = any(
                getattr(self, f"search_{field[0]}").currentText().strip()
                for field in self.field_mappings
                if field[0] in ["_branch"]
            )

            if not has_custom_branch:
                # 递归获取当前节点及其所有子节点的原始名称
                def get_all_original_names(item):
                    names = []
                    display_text = item.text(0)
                    parts = display_text.split(" ", 1)
                    original_name = parts[1].strip() if len(parts) > 1 else display_text.strip()
                    names.append(original_name)
                    for i in range(item.childCount()):
                        child = item.child(i)
                        names.extend(get_all_original_names(child))
                    return names

                original_names = get_all_original_names(current_item)
                if original_names:
                    # 使用field_branch字段，并构造IN条件
                    placeholders = ", ".join(["?"] * len(original_names))
                    conditions.append(f"field_branch IN ({placeholders})")
                    params.extend(original_names)

        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
        return where_clause, params

    # 新增删除数据方法
    def delete_all_data(self):
        # 安全确认对话框
        confirm = QMessageBox.warning(
            self,
            "危险操作确认",
            "即将删除全部专利数据！\n此操作不可撤销！", # ⚠️
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.No
        )

        if confirm != QMessageBox.StandardButton.Yes:
            return

        try:
            # 获取删除前的总记录数
            count_query = QSqlQuery("SELECT COUNT(*) FROM patents")
            count_query.next()
            total_records = count_query.value(0)

            if total_records == 0:
                QMessageBox.information(self, "提示", "数据库当前为空，无需删除")
                return

            # 执行删除操作
            self.db.transaction()
            delete_query = QSqlQuery()

            if not delete_query.exec("DELETE FROM patents"):
                raise Exception(delete_query.lastError().text())

            self.db.commit()

            # 刷新界面
            self.load_tree()
            self.load_data()

            # 操作结果提示
            QMessageBox.information(
                self,
                "删除成功",
                f"已删除全部数据，共清除 {total_records} 条记录",
                QMessageBox.StandardButton.Ok
            )

        except Exception as e:
            self.db.rollback()
            QMessageBox.critical(
                self,
                "删除失败",
                f"数据删除过程中发生错误：\n{str(e)}",
                QMessageBox.StandardButton.Ok
            )

    def import_excel(self):
        """优化后的批量导入方法（兼容旧版pandas）"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)")

        if not file_path:
            return

        try:
            # 全量读取（适用于小文件）
            df = pd.read_excel(file_path)
            df = df.where(pd.notnull(df), None)
            records = df.to_dict('records')
            batch_size = 200  # 每批插入数量
            total_success = 0
            total_records = len(records)

            # 进度对话框
            progress = QProgressDialog("导入数据中...", "取消", 0, total_records, self)
            progress.setWindowTitle("请稍候")
            progress.setWindowModality(Qt.WindowModality.WindowModal)
            progress.show()  # 强制显示对话框

            for i in range(0, total_records, batch_size):
                # 检查取消操作
                if progress.wasCanceled():
                    break
                # 关键修改点1: 在循环开始立即处理事件
                QCoreApplication.processEvents()
                # 获取当前批次
                batch = records[i:i + batch_size]
                # 开始事务
                self.db.transaction()
                query = QSqlQuery()
                # 14 * 4 +2 =58
                query_str = """
                    INSERT INTO patents VALUES (
                        ?,?,?,?,?,?,?,?,?,?,?,?,?,?,
                        ?,?,?,?,?,?,?,?,?,?,?,?,?,?,
                        ?,?,?,?,?,?,?,?,?,?,?,?,?,?,
                        ?,?,?,?,?,?,?,?,?,?,?,?,?,?,
                        ?,?
                    )
                """
                query.prepare(query_str)
                batch_success = 0
                for record in batch:
                    # 绑定参数
                    params = [record.get(col, None) for col in
                              [x[0] for x in self.field_mappings if isinstance(x, (tuple, list)) and len(x) >= 1]]

                    for idx, val in enumerate(params):
                        query.bindValue(idx, val)

                    if query.exec():
                        batch_success += 1
                    else:
                        self.db.rollback()
                        raise Exception(f"插入失败: {query.lastError().text()}")
                # 提交批次
                if self.db.commit():
                    total_success += batch_success
                    progress.setValue(min(i + batch_size, total_records))
                    # 关键修改点2: 更新进度并强制刷新界面
                    QCoreApplication.processEvents()  # 立即处理界面事件
                else:
                    self.db.rollback()
                    raise Exception("事务提交失败")
            if 'query' in locals(): query.clear()
            progress.close()
            QMessageBox.information(
                self, '完成',
                f"成功导入{total_success}/{total_records}条数据！"
            )
            self.load_tree()
            self.load_data()

        except Exception as e:
            self.db.rollback()
            QMessageBox.critical(
                self, '错误',
                f'导入过程中发生异常: {str(e)}'
            )

    def export_excel(self):
        """优化后的流式导出方法（带进度条）"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, "导出Excel", "", "Excel Files (*.xlsx)")

        if not file_path:
            return

        progress = None
        writer = None
        try:
            # 获取查询条件
            where_clause, params = self.get_query_conditions()

            # 优化WHERE子句格式
            where_clause = where_clause.strip()
            if where_clause and not where_clause.startswith("WHERE"):
                where_clause = f"WHERE {where_clause}"

            # 先获取列名（确保空数据时也能正确显示列头）
            column_query = QSqlQuery()
            column_query_str = "SELECT * FROM patents LIMIT 0"  # 空查询获取元数据

            if not column_query.exec(column_query_str):
                raise Exception(f"获取列名失败: {column_query.lastError().text()}")
            columns = [column_query.record().fieldName(i) for i in range(column_query.record().count())]

            # 查询总记录数（带条件）
            count_query = QSqlQuery()
            count_query_str = f"SELECT COUNT(*) FROM patents {where_clause}"
            if not count_query.prepare(count_query_str):
                raise Exception(f"预处理失败: {count_query.lastError().text()}")
            for i, param in enumerate(params):
                count_query.bindValue(i, param)
            if not count_query.exec():
                raise Exception(f"获取总数失败: {count_query.lastError().text()}")
            count_query.next()
            total_records = count_query.value(0)

            # 初始化进度条
            progress = QProgressDialog("导出数据中...", "取消", 0, total_records, self)
            progress.setWindowModality(Qt.WindowModality.WindowModal)
            progress.show()
            QCoreApplication.processEvents()

            # 创建Excel写入器
            writer = pd.ExcelWriter(file_path, engine='openpyxl', mode='w')
            page_size = 5000
            current_page = 1
            total_exported = 0

            while True:
                if progress.wasCanceled():
                    writer.close()
                    os.remove(file_path)
                    QMessageBox.information(self, "取消", "导出操作已取消")
                    return

                # 构造分页查询
                offset = (current_page - 1) * page_size
                sort_column = self.column_mapping[self.current_sort_column]
                sort_order = 'ASC' if self.current_sort_order == Qt.SortOrder.AscendingOrder else 'DESC'

                query_str = f"""
                    SELECT * FROM patents
                    {where_clause}
                    ORDER BY {sort_column} {sort_order}
                    LIMIT {page_size} OFFSET {offset}
                """.strip().replace("\n", " ")

                query = QSqlQuery()
                if not query.prepare(query_str):
                    raise Exception(f"查询预处理失败: {query.lastError().text()}")

                # 绑定参数（仅当有参数时）
                for i, p in enumerate(params):
                    query.bindValue(i, p)

                if not query.exec():
                    raise Exception(f"查询失败: {query.lastError().text()}")

                # 转换数据
                rows = []
                while query.next():
                    rows.append([query.value(i) for i in range(len(columns))])

                # 写入Excel
                if rows:
                    df = pd.DataFrame(rows, columns=columns)
                    start_row = total_exported + 1 if current_page > 1 else 0
                    df.to_excel(writer, startrow=start_row, header=(current_page == 1), index=False)
                    total_exported += len(rows)
                    progress.setValue(total_exported)
                    QCoreApplication.processEvents()

                current_page += 1
                if not rows:
                    break

            # 处理完全空数据情况
            if total_exported == 0:
                pd.DataFrame(columns=columns).to_excel(writer, index=False)

            writer.close()
            progress.close()
            QMessageBox.information(self, '成功', f"成功导出{total_exported}条数据！")

        except Exception as e:
            if writer: writer.close()
            if progress: progress.close()
            QMessageBox.critical(self, '错误', f'导出失败: {str(e)}')
        finally:
            if 'query' in locals(): query.clear()

    def init_pdf_viewer(self):
        """初始化PDF查看器（单例）"""
        if not hasattr(self, 'pdf_window'):
            # os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"  # 禁用 GPU 加速.如果不禁用,加载pdf时要闪退.
            # 使用软件渲染 + 禁用 GPU
            os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --enable-software-rasterizer"
            self.pdf_window = QMainWindow()
            self.pdf_window.setWindowTitle("PDF浏览器")
            self.pdf_window.resize(1200, 800)

            # WebEngineView配置
            self.web_view = QWebEngineView()
            settings = self.web_view.settings()
            # # PySide6 中枚举值通过 QWebEngineSettings 直接访问  允许访问本地文件（替代 LocalContentCanAccessFileUrls）
            settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
            settings.setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
            settings.setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, True)

            # 工具栏
            toolbar = self.pdf_window.addToolBar("操作")
            self.download_action = QAction("下载PDF", self.pdf_window)
            toolbar.addAction(self.download_action)
            self.pdf_window.setCentralWidget(self.web_view)
            # 关闭时清空内容
            self.pdf_window.closeEvent = lambda e: self.web_view.load(QUrl())

    def view_pdf(self, pdf_path):
        """查看PDF（优化版）"""
        if not os.path.exists(pdf_path):
            QMessageBox.warning(self, '错误', 'PDF文件不存在！')
            return

        # 初始化查看器
        if not hasattr(self, 'pdf_window'):
            self.init_pdf_viewer()

        # 显示加载状态
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        self.statusBar().showMessage("正在加载PDF...")
        QCoreApplication.processEvents()  # 立即更新界面

        try:
            # 绑定当前PDF路径到下载按钮
            self.download_action.triggered.disconnect()
            self.download_action.triggered.connect(lambda: self.download_pdf(pdf_path))
            pdf_url = QUrl.fromLocalFile(os.path.abspath(pdf_path))
            self.web_view.load(pdf_url)
            self.pdf_window.show()
        except Exception as e:
            QMessageBox.critical(self, '错误', f'加载失败: {str(e)}')
        finally:
            QApplication.restoreOverrideCursor()
            self.statusBar().clearMessage()

    def download_pdf(self, source_path):
        # 检查源文件是否存在
        if not os.path.exists(source_path):
            QMessageBox.warning(self, '错误', '原始PDF文件不存在！')
            return

        # 获取保存路径
        default_name = os.path.basename(source_path)
        save_path, _ = QFileDialog.getSaveFileName(
            self,
            "保存PDF",
            os.path.join(os.path.expanduser("~"), default_name),
            "PDF文件 (*.pdf)"
        )

        if not save_path:
            return  # 用户取消保存

        try:
            # 复制文件到指定路径
            shutil.copyfile(source_path, save_path)
            QMessageBox.information(
                self,
                "保存成功",
                f"文件已成功保存到：\n{save_path}"
            )
        except PermissionError:
            QMessageBox.critical(
                self,
                "保存失败",
                "没有写入权限，请选择其他路径"
            )
        except Exception as e:
            QMessageBox.critical(
                self,
                "保存失败",
                f"保存文件时发生错误：\n{str(e)}"
            )

    def on_tree_click(self, item):
        self.current_page = 1
        self.load_data()

    def on_search_changed(self):
        # 处理搜索条件变化
        self.current_page = 1
        self.load_data()

    def prev_page(self):
        if self.current_page > 1:
            self.current_page -= 1
            self.load_data()

    def next_page(self):
        if self.current_page < self.total_pages:
            self.current_page += 1
            self.load_data()

    def on_page_size_changed(self):
        self.page_size = int(self.cmb_page_size.currentText())
        self.current_page = 1
        self.load_data()


